﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SelectionAddin.Completer;
using Roslyn.Compilers.CSharp;
using System.Threading;
using System.Threading.Tasks;
using CompleterLib.Util;
using Util;

namespace CompleterLib.AbstractTypes
{
    public class AbstractTypeInferencer
    {
        private readonly SemanticModel semModel;
        private readonly ConstraintCollector cc;
        private Dictionary<TypeTerm, TypeTerm> localTermMeanings;
        private readonly CancellationToken token;
        private static CancellationTokenSource externalCancellationSource;
        private static Task<Dictionary<TypeTerm, TypeTerm>> externalTermMeanings = null;

        public AbstractTypeInferencer(SemanticModel semModel, int position, CancellationToken token)
        {
            this.semModel = semModel;
            this.token = token;
            this.cc = ConstraintCollector.GetForCompilation(semModel.Compilation);
            if (externalTermMeanings == null
                || externalTermMeanings.IsCanceled
                || externalTermMeanings.IsFaulted)
            {
                // TODO Start this in a separate thread when a project is loaded.
                // TODO Cache parsed files so re-running is faster?
                //      Have to be careful to not have stale data in cache.
                BeginGenerateExternalTermMeanings(semModel, position);
            }
            var localTermMeanings = GetTermMeanings(cc.Constraints);
            if (externalTermMeanings.Status.Equals(TaskStatus.RanToCompletion))
            {
                this.localTermMeanings = MergeTermMeanings(localTermMeanings,
                                                           externalTermMeanings.Result);
            }
            else
            {
                this.localTermMeanings = localTermMeanings;
            }
        }

        private static Dictionary<TypeTerm, TypeTerm> GenerateExternalTermMeanings(
                SemanticModel semModel, int position, CancellationToken token)
        {
            IEnumerable<TypeConstraint> externalContraints =
                new TypeTermParser(semModel, position)
                    .GetExternalTypeConstraints(semModel.Compilation.GetAllReferences(), token);
            return GetTermMeanings(externalContraints);
        }
        private static Lazy<Dictionary<TypeTerm, TypeTerm>> LazyGenerateExternalTermMeanings(
            SemanticModel semModel, CancellationToken token, int position = 0)
        {
            return new Lazy<Dictionary<TypeTerm, TypeTerm>>(
                () => GenerateExternalTermMeanings(semModel, position, token));
        }
        private static void BeginGenerateExternalTermMeanings(
            SemanticModel semModel, int position = 0)
        {
            if (externalCancellationSource != null)
            {
                externalCancellationSource.Cancel();
            }
            externalCancellationSource = new CancellationTokenSource();
            var token = externalCancellationSource.Token;
            externalTermMeanings = new Task<Dictionary<TypeTerm, TypeTerm>>(
                () => GenerateExternalTermMeanings(semModel, position, token),
                token,
                TaskCreationOptions.LongRunning);
            try
            {
                externalTermMeanings.Start();
            }
            catch (InvalidOperationException)
            {
                token.ThrowIfCancellationRequested();
            }
        }


        public bool TryGetTermMeaning(TypeTerm key, out TypeTerm meaning)
        {
            /*if (externalTermMeanings == null)
            {
                externalTermMeanings = GenerateExternalTermMeanings(semModel, 0, token);
                var localTermMeanings = GetTermMeanings(cc.Constraints);
                this.localTermMeanings = MergeTermMeanings(localTermMeanings,
                                                           externalTermMeanings);
            }*/
            if (!this.localTermMeanings.TryGetValue(key, out meaning))
            {
                if (externalTermMeanings.Status.Equals(TaskStatus.RanToCompletion))
                {
                    return externalTermMeanings.Result.TryGetValue(key, out meaning);
                }
                else
                {
                    return false;
                }
            }
            else
            {
                return true;
            }
        }
        public bool IsSameAbstractType(object left, object right)
        {
            throw new Exception("Not implemented");
        }
        public bool IsSameAbstractType(TypeTerm left, TypeTerm right)
        {
            TypeTerm leftTT, rightTT;
            // XXX TODO
            if (TryGetTermMeaning(left, out leftTT)
                && TryGetTermMeaning(right, out rightTT))
            {
                return leftTT.Equals(rightTT);
            }
            else
            {
                return false;
            }
        }
        internal bool IsSameArg(Completion comp, MethodSymbol meth, int index)
        {
            return IsSameAbstractType(new FormalParameterTypeTerm(meth, index),
                                      comp.TypeTerm);   
        }
        internal bool IsSameArg(Completion comp, IMethodSymbolWrapper meth, int index)
        {
            if (meth is MethodSymbolWrapper)
            {
                var symbol = (meth as MethodSymbolWrapper).RealSymbol;
                return IsSameAbstractType(new FormalParameterTypeTerm(symbol, index),
                                          comp.TypeTerm);
            }
            else
            {
                return true; // XXX TODO ??
            }
        }

        private static Dictionary<TypeTerm, TypeTerm> GetTermMeanings(IEnumerable<TypeConstraint> cs)
        {
            Queue<TypeConstraint> constraints
                = new Queue<TypeConstraint>(cs);

            Dictionary<TypeTerm, TypeTerm> termMeanings
                = constraints.Select(c => c.Left).Union(constraints.Select(c => c.Right))
                             //.Where(tt => tt is IdentifiedTypeTerm)
                             //.Cast<IdentifiedTypeTerm>()
                             .Distinct()
                             .ToDictionary(t => t, t => t);
            var disjointSets = new DisjointSets<TypeTerm>(
                new HashSet<TypeTerm>(
                    new IEnumerable<TypeTerm>[] {
                        termMeanings.Keys,
                        termMeanings.Values, 
                        constraints.Select(c => c.Left),
                        constraints.Select(c => c.Right),
                    }.Concat()));
            foreach (var c in constraints)
            {
                disjointSets.Union(c.Left, c.Right);
            }
            termMeanings = termMeanings.ToDictionary(kv => kv.Key,
                    kv => disjointSets.FindRoot(kv.Value));
            return termMeanings;
        }

        private Dictionary<TypeTerm, TypeTerm> MergeTermMeanings(
                Dictionary<TypeTerm, TypeTerm> localTermMeanings,
                Dictionary<TypeTerm, TypeTerm> externalTermMeanings)
        {
            Dictionary<TypeTerm, TypeTerm> local = new Dictionary<TypeTerm,TypeTerm>();
            foreach (var group in localTermMeanings
                .ToLookup(kv => kv.Value, kv => kv.Key)
                .Select(g => g.Key.Singleton()
                            .Concat(g.Cast<TypeTerm>())
                            .Distinct()
                            .ToList()))
            {
                var matchingTT = group.FirstOrDefault(tt => externalTermMeanings.ContainsKey(tt));
                TypeTerm match = matchingTT == null
                    ? group.First()
                    : externalTermMeanings[matchingTT];
                    foreach(var tt in group) {
                        local.Add(tt, match);
                    }
            }
            return local;
        }

        internal TypeTerm GetTypeTermFor(ExpressionSyntax exp, SemanticModel semModel)
        {
            return cc.GetTypeTermFor(exp, semModel);
        }
        internal TypeTerm GetTypeTermFor(Symbol sym)
        {
            return cc.GetTypeTermForDefinition(sym);
        }
        private TypeTerm GetTypeTermFor(ExpressionSyntax exp)
        {
            return GetTypeTermFor(exp, semModel);
        }

        internal static void ClearExternalAbsTypes(SemanticModel semModel)
        {
            BeginGenerateExternalTermMeanings(semModel);
        }
    }
}
